'''
用于认证功能
'''
import functools

from flask import (
    Blueprint, flash, g, redirect, render_template, request, session, url_for
)
# Blueprint 是一种组织一组相关视图及其他代码的方式.与把视图及其他代码直接注册到应用的方式不同,蓝图方式是把它们注册到蓝图,然后在工厂函数中把蓝图注册到应用.
# 当使用蓝图的时候,蓝图的名称会添加到函数名称的前面.下面的 login 函数的端点为 'auth.login' ,因为它已被加入 'auth' 蓝图中.
from werkzeug.security import check_password_hash, generate_password_hash

from flaskr.db import get_db

bp = Blueprint('auth', __name__, url_prefix='/auth')
# 这里创建了一个名称为'auth'的Blueprint.和应用对象一样,蓝图需要知道是在哪里定义的,因此把__name__作为函数的第二个参数.url_prefix会添加到所有与该蓝图关联的URL前面.

# 当访问/auth/registerURL时,register视图会返回用于填写注册内容的表单的HTML.当用户提交表单时,视图会验证表单内容,然后要么再次显示表单并显示一个出错信息,要么创建新用户并显示登录页面.
@bp.route('/register', methods=('GET', 'POST'))
# @bp.route关联了URL/register和register视图函数.当Flask收到一个指向/auth/register的请求时就会调用register视图并把其返回值作为响应.
def register():
    if request.method == 'POST':
        # 如果用户提交了表单,那么request.method 将会是'POST'.这种情况下会开始验证用户的输入内容.
        username = request.form['username']
        password = request.form['password']
        # request.form是一个特殊类型的dict,其映射了提交表单的键和值.表单中,用户将会输入其username和password.
        db = get_db()
        error = None

        if not username:
            error = 'Username is required.'
        elif not password:
            error = 'Password is required.'
        # 验证 username 和 password 不为空。
        elif db.execute(
            'SELECT id FROM user WHERE username = ?', (username,)
        ).fetchone() is not None:
            error = 'User {} is already registered.'.format(username)
        # 通过查询数据库,检查是否有查询结果返回来验证 username 是否已被注册. db.execute 使用了带有 ? 占位符的 SQL 查询语句.占位符可以代替后面的元组参数中相应的值.使用占位符的好处是会自动帮你转义输入值,以抵御 SQL 注入攻击.
        # fetchone() 根据查询返回一个记录行,如果查询没有结果,则返回 None . fetchall()返回包括所有结果的列表.

        if error is None:
            db.execute(
                'INSERT INTO user (username, password) VALUES (?, ?)',
                (username, generate_password_hash(password))
            )
            # 如果验证成功,那么在数据库中插入新用户数据.为了安全原因,不能把密码明文储存在数据库中.相代替的,使用 generate_password_hash() 生成安全的哈希值并储存到数据库中.
            db.commit()
            # 查询修改了数据库的数据后使用 db.commit() 保存修改.
            # 用户数据保存后将转到登录页面. url_for() 根据登录视图的名称生成相应的 URL .与写固定的 URL 相比,这样做的好处是如果以后需要修改该视图相应的 URL,那么不用修改所有涉及到 URL 的代码.redirect() 为生成的 URL 生成一个重定向响应.
            return redirect(url_for('auth.login'))

        flash(error)
        # 如果验证失败,那么会向用户显示一个出错信息.flash() 用于储存在渲染模块时可以调用的信息.

    # 当用户最初访问 auth/register 时,或者注册出错时,应用显示一个注册表单. render_template() 会渲染一个包含 HTML 的模板.
    return render_template('auth/register.html')

@bp.route('/login', methods=('GET', 'POST'))
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        error = None
        user = db.execute(
            'SELECT * FROM user WHERE username = ?', (username,)
        ).fetchone()
        # 首先需要查询用户并存放在变量中，以备后用.

        if user is None:
            error = 'Incorrect username.'
        elif not check_password_hash(user['password'], password):
            # check_password_hash() 以相同的方式哈希提交的密码并安全的比较哈希值.如果匹配成功,那么密码就是正确的.
            error = 'Incorrect password.'

        if error is None:
            session.clear()
            session['user_id'] = user['id']
            # session 是一个 dict ,它用于储存横跨请求的值.当验证成功后,用户的 id 被储存于一个新的会话中.会话数据被储存到一个向浏览器发送的 cookie 中,在后继请求中,浏览器会返回它. Flask 会安全对数据进行签名以防数据被篡改.
            # 现在用户的 id 已被储存在 session 中,可以被后续的请求使用.请每个请求的开头,如果用户已登录,那么其用户信息应当被载入,以使其可用于其他视图。
            return redirect(url_for('index'))

        flash(error)

    return render_template('auth/login.html')

# bp.before_app_request() 注册一个在视图函数之前运行的函数,不论其 URL 是什么.
# load_logged_in_user 检查用户 id 是否已经储存在 session 中,并从数据库中获取用户数据,然后储存在 g.user 中.
# g.user 的持续时间比请求要长. 如果没有用户 id ,或者 id 不存在,那么 g.user 将会是 None .
@bp.before_app_request
def load_logged_in_user():
    user_id = session.get('user_id')

    if user_id is None:
        g.user = None
    else:
        g.user = get_db().execute(
            'SELECT * FROM user WHERE id = ?', (user_id,)
        ).fetchone()

# 注销的时候需要把用户 id 从 session 中移除.然后 load_logged_in_user 就不会在后继请求中载入用户了。
@bp.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('index'))

# 用户登录以后才能创建、编辑和删除博客帖子.在每个视图中可以使用装饰器来完成这个工作.
def login_required(view):
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return redirect(url_for('auth.login'))

        return view(**kwargs)
    # 装饰器返回一个新的视图，该视图包含了传递给装饰器的原视图.新的函数检查用户是否已载入.如果已载入,那么就继续正常执行原视图,否则就重定向到登录页面.
    return wrapped_view
